package edu.cmu.javadb;

import java.util.*;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.annotation.*;
import java.lang.reflect.*;

import edu.cmu.javadb.exceptions.NoAttributeException;
import edu.cmu.javadb.exceptions.NotComparableException;
import edu.cmu.javadb.exceptions.NotFlattenableException;
import edu.cmu.javadb.index.Index;
import edu.cmu.javadb.index.TreeSetIndex;
import edu.cmu.javadb.plan.*;
import edu.cmu.javadb.plan.condition.Condition;
import edu.cmu.javadb.plan.condition.Path;
import static edu.cmu.javadb.plan.condition.Condition.*;

public class VCollection<T extends BaseRecord> implements Iterable<T>, Collection<T>, Queryable<T>, PropertyChangeListener {
	private Collection<T> records = new HashSet<T>();

	private Map<IndexInfo, Index<T>> indexes = new HashMap<IndexInfo, Index<T>>();

	// protected List<MView<T>> children = new ArrayList<MView<T>>();

	private Class<?> type = null;

	private RecordInfo recordInfo = null;
	
	private List<Object> backLinks = new ArrayList<Object>();

	private void initialize(Class<?> type) {
		this.type = type;

		// if the class haven't been registered globally, register it now!
		recordInfo = RecordInfo.getRecordInfo(type);
		if (recordInfo == null) {
			try {
				RecordInfo.RegisterRecordInfo(type);
			} catch (NotComparableException e) {
				e.printStackTrace();
			}
			recordInfo = RecordInfo.getRecordInfo(type);
		}

		// create actual indexes

		for (IndexInfo ii : recordInfo.getIndexInfo()) {
			Index<T> i = new TreeSetIndex<T>(type, ii);
			indexes.put(ii, i);
		}
	}
	
	static Map<Class<?>, VCollection<?>> collections = new HashMap<Class<?>, VCollection<?>>();
	
	static public <T extends BaseRecord> VCollection<T> getInstance(Class<?> type) {
		if (!collections.containsKey(type)) {
			collections.put(type, new VCollection<T>(type));
		}
		
		VCollection<T> res = (VCollection<T>) collections.get(type);
		return res;
	}

	private VCollection(Class<?> type) {
		initialize(type);
	}
	
	private VCollection() {

	}

	public View<T> filter(Condition cond) {
		View<T> res = new View<T>(new Selection(this.getPlan(), cond));
		return res;
	}

	@Override
	public <T2> View<T2> project(Path path) {
		View<T2> res = new View<T2>(new Projection(this.getPlan(), Arrays.asList(path)));
		return res;
	}

	@Override
	public <T2> View<T> join(Queryable<T2> c2, Path p1, Path p2) {
		View<T> res = new View<T>(new Join(this.getPlan(), p1, c2.getPlan(), p2));	
		return res;
	}

	public Collection<T> query(Condition cond) {
		Collection<T> res = new HashSet<T>();

		RecordInfo ri = RecordInfo.getRecordInfo(type);
		boolean useIndex = false;

		for (IndexInfo ii : ri.getIndexInfo()) {
			if (!cond.containRootPath() && !cond.containNonValBinary() && ii.isCompatible(cond.getAllFields(ri))) {
				useIndex = true;
				Index<T> index = indexes.get(ii);
				try {
					return index.find(cond);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}

		if (useIndex == false) {
			System.out.println("Not using index");
			for (T rec : records) {
				if (cond.satisfy(rec)) {
					res.add(rec);
				}
			}
		}
		return res;
	}

	@Override
	public boolean add(T r) {
		if (type == null) {
			this.initialize(r.getClass());
		}
		records.add(r);
		addToAllIndexes(r);
		addToAllLink(r);
		r.addPropertyChangeListener(this);
		return true;
	}
	
	@Override
	public boolean remove(Object o) {
		records.remove(o);
		T r = (T) o;
		removeFromAllIndexes(r);
		r.removePropertyChangeListener(this);

		// for (VCollection<T> child : children) {
		// child.remove(r);
		// }
		return true;
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		update((T) evt.getSource(), evt.getPropertyName(), evt.getNewValue());
	}
	

	public void update(T r, String attName, Object newVal) {
		try {
			Attribute att = recordInfo.getAttribute(attName);

			// Object oldVal = att.getValue(r);

			for (IndexInfo ii : indexes.keySet()) {
				if (ii.containAttribute(att)) {
					Index<T> i = indexes.get(ii);
					i.remove(r);
				}
			}

			// Afer update the value, we rebuild the index
			// att.setValue(r, newVal);
			att.setFieldDirectly(r, newVal);

			for (IndexInfo ii : indexes.keySet()) {
				if (ii.containAttribute(att)) {
					Index<T> i = indexes.get(ii);
					i.add(r);
				}
			}
			
			if (att.getLinked() != null) {
				att.addToLink(r);
			}

			// att.setFieldDirectly(r, oldVal); // recover the oldValue, since
			// the
			// // val will be updated in target
			// // program

			// for (MView<T> child : children) {
			// TODO child.update(r, attName, newVal);
			// }
		} catch (NoAttributeException e) {
			e.printStackTrace();
		}
	}

	@Override
	public <T2> Queryable<T2> flatten() throws NotFlattenableException {
		View<T2> res = new View<T2>(new Flattenation(this.getPlan()));
		return res;
	}

	public void addToAllIndexes(T r) {
		for (Index<T> i : indexes.values()) {
			i.add(r);
		}
	}
	
	public <T2 extends BaseRecord> void addToAllLink(T r) {		
		for (Attribute att : recordInfo.getAttributes().values()) {
			if (att.getLinked() != null) {
				att.addToLink(r);
			}
		}
	}
	
	protected void addBackLink(Object o) {
		backLinks.add(o);
	}
	
	protected void removeBackLink(T r) {
		for (Object o : backLinks) {
 
		}
	}

	public void removeFromAllIndexes(T r) {
		for (Index<T> i : indexes.values()) {
			i.remove(r);
		}

		// for (MView<T> child : children) {
		// child.remove(r);
		// }
	}

	@Override
	public Iterator<T> iterator() {
		return records.iterator();
	}

	@Override
	public boolean addAll(Collection<? extends T> arg0) {
		for (T t : arg0) {
			this.add(t);
		}
		return false;
	}

	@Override
	public void clear() {
		// TODO: check if this is correct
		for (T r : records) {
			this.remove(r);
		}
	}

	@Override
	public boolean contains(Object arg0) {
		return this.records.contains(arg0);
	}

	@Override
	public boolean containsAll(Collection<?> arg0) {
		return this.records.containsAll(arg0);
	}

	@Override
	public boolean isEmpty() {
		return this.records.isEmpty();
	}

	@Override
	public boolean removeAll(Collection<?> arg0) {
		for (Object o : arg0) {
			this.remove(o);
		}
		return false;
	}

	@Override
	public boolean retainAll(Collection<?> arg0) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public int size() {
		return records.size();
	}

	@Override
	public Object[] toArray() {
		return records.toArray();
	}

	@Override
	public <T> T[] toArray(T[] arg0) {	//TODO: SEE HOW OTHERS SOLVE THE PROBLEM
		return records.toArray(arg0);
	}

	@Override
	public Collection<T> execute() {
		return this;
	}

	private PlanNode plan = new Table(this);

	@Override
	public PlanNode getPlan() {
		return plan;
	}

	@Override
	public Queryable<T> union(Queryable<T>... args) {
		Union u = new Union();
		u.addChildren(this.getPlan());
		for (Queryable<T> arg : args) {
			u.addChildren(arg.getPlan());
		}
		View<T> res = new View<T>(u);
		return res;
	}

	@Override
	public <K> VTable<K, T> groupBy(Path path) {
		// TODO Auto-generated method stub
		return null;
	}

	@Override
	public <S> S reduce(ReduceFn<S, T> fn) {
		// TODO Auto-generated method stub
		return null;
	}
}
